import * as vue from 'vue';
import { version as vue3Version } from 'vue';
import type { DirectiveBinding } from 'vue';
const version = vue3Version || vue.version;
declare global {
  interface Navigator {
    msMaxTouchPoints: number;
  }
}
interface BindingValue {
  eventName: keyof HTMLElementEventMap;
  srcTarget: HTMLElement | Window;
  handler: (event: Event) => void;
  capture: boolean;
  middleware?: (event: Event) => boolean;
  el?: ClickOutsideElement;
  event?: MouseEvent;
  events?: (keyof HTMLElementEventMap)[];
  isActive?: boolean;
  detectIframe?: boolean;
}
interface ClickOutsideElement extends HTMLElement {
  '__v-click-outside'?: BindingValue[];
}

const isVue3 = /^3\./.test(version);
const directiveHooks = {
  beforeMount: isVue3 ? 'beforeMount' : 'bind',
  updated: isVue3 ? 'updated' : 'update',
  unmounted: isVue3 ? 'unmounted' : 'unbind',
};
const HANDLERS_PROPERTY = '__v-click-outside';
const HAS_WINDOWS = typeof window !== 'undefined';
const HAS_NAVIGATOR = typeof navigator !== 'undefined';
const IS_TOUCH =
  HAS_WINDOWS && ('ontouchstart' in window || (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0));
const EVENTS = (IS_TOUCH ? ['touchstart'] : ['click']) as (keyof HTMLElementEventMap)[];

function processDirectiveArguments(bindingValue: BindingValue) {
  const isFunction = typeof bindingValue === 'function';
  if (!isFunction && typeof bindingValue !== 'object') {
    throw new Error('v-click-outside: Binding value must be a function or an object');
  }

  return {
    handler: isFunction ? bindingValue : bindingValue.handler,
    middleware: bindingValue.middleware || ((item: any) => item),
    events: bindingValue.events || EVENTS,
    isActive: !(bindingValue.isActive === false),
    detectIframe: !(bindingValue.detectIframe === false),
    capture: !!bindingValue.capture,
  };
}

function execHandler({ event, handler, middleware }: BindingValue) {
  if (event && middleware!(event)) {
    handler(event);
  }
}

function onFauxIframeClick({ el, event, handler, middleware }: BindingValue) {
  // Note: on firefox clicking on iframe triggers blur, but only on
  //       next event loop it becomes document.activeElement
  // https://stackoverflow.com/q/2381336#comment61192398_23231136
  setTimeout(() => {
    const { activeElement } = document;
    if (activeElement && activeElement.tagName === 'IFRAME' && !el!.contains(activeElement)) {
      execHandler({ event, handler, middleware } as BindingValue);
    }
  }, 0);
}

function onEvent({ el, event, handler, middleware }: BindingValue) {
  // Note: composedPath is not supported on IE and Edge, more information here:
  //       https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath
  //       In the meanwhile, we are using el.contains for those browsers, not
  //       the ideal solution, but using IE or EDGE is not ideal either.
  if (!event || !el) return;
  //@ts-ignore
  const path = (event.composedPath && event.composedPath()) || event.path;
  const isClickOutside = path ? path.indexOf(el) < 0 : !el.contains(event.target as Node);

  if (!isClickOutside) {
    return;
  }

  execHandler({ event, handler, middleware } as BindingValue);
}

function bind(el: ClickOutsideElement, bingValue: DirectiveBinding) {
  console.log("click outsider")
  const { events, handler, middleware, isActive, detectIframe, capture } =
    processDirectiveArguments(bingValue.value);
  if (!isActive) {
    return;
  }

  el[HANDLERS_PROPERTY] = events.map(eventName => ({
    eventName: eventName,
    srcTarget: document.documentElement,
    handler: (event: Event) => onEvent({ el, event, handler, middleware } as BindingValue),
    capture,
  }));

  if (detectIframe) {
    const detectIframeEvent: BindingValue = {
      eventName: 'blur',
      srcTarget: window,
      handler: (event: Event) =>
        onFauxIframeClick({ el, event, handler, middleware } as BindingValue),
      capture,
    };

    el[HANDLERS_PROPERTY] = [...el[HANDLERS_PROPERTY], detectIframeEvent];
  }

  el[HANDLERS_PROPERTY].forEach(({ eventName, srcTarget, handler }: BindingValue) =>
    setTimeout(() => {
      // Note: More info about this implementation can be found here:
      //       https://github.com/ndelvalle/v-click-outside/issues/137
      if (!el[HANDLERS_PROPERTY]) {
        return;
      }
      (srcTarget as HTMLElement).addEventListener(eventName, handler, capture);
    }, 0)
  );
}

function unbind(el: ClickOutsideElement) {
  const handlers = el[HANDLERS_PROPERTY] || [];
  handlers.forEach(({ eventName, srcTarget, handler, capture }) =>
    (srcTarget as HTMLElement).removeEventListener(eventName, handler, capture)
  );
  delete el[HANDLERS_PROPERTY];
}

function update(el: ClickOutsideElement, { value, oldValue }: DirectiveBinding) {
  if (JSON.stringify(value) === JSON.stringify(oldValue)) {
    return;
  }
  unbind(el);
  bind(el, { value } as DirectiveBinding);
}

const directive = {
  [directiveHooks.beforeMount]: bind,
  [directiveHooks.updated]: update,
  [directiveHooks.unmounted]: unbind,
};

export default HAS_WINDOWS ? directive : {};
